dump("// $Id: test_NumberConversion.js,v 1.82 2007/02/28 11:18:33 Will Exp $;\n");

var WILLTEST = top.WILLTEST;

var assertCausesException  = WILLTEST.assertCausesException;
var assertFailCreateObject = WILLTEST.assertFailCreateObject;
var assertCreateObject     = WILLTEST.assertCreateObject;
var assertValidRef         = WILLTEST.assertValidRef;
var assertArraysEqual      = WILLTEST.assertArraysEqual;
var traceTest              = WILLTEST.traceTest;
var newListFromCodes       = WILLTEST.newListFromCodes;

//Use the name of the tests you want to run, for example "testExchangeRateCache,testNumberSwissQuote";
// (if it's already set in another file then it won't be overridden)
traceTest.limitTo = traceTest.limitTo || ""; 

traceTest.deleberateExceptionsAllowed = true;

if (!VMC) { alert('no vmc'); }

const nbsp='\u00A0';

//redefine VMC.log to use the JS Unit logging system. WILL: TODO: shouldn't this go centrally?
VMC.log = function (severity,message) {
  //quote HTML
  message=message.replace(/</g,'&lt;');
  message=message.replace(/>/g,'&gt;');
  var text=severity+': '+message;
  switch(severity) {
  case 'INFO' : info(text);  break;
  case 'WARN' : warn(text);  break;
  case 'ERR'  : warn(text);  throw text; break; // VMC would also have thrown an exception
  default     : debug(text);
  }
};


var gSourceCurrencyList;
var gConversionProfile;
var gConversionProfileTable;

var setSourceTo = function(currencyCodeList) {
  if (currencyCodeList=='COMPLETELIST') {   gSourceCurrencyList = VMC.currencyDB.getCompleteList(); }
  else {                                    gSourceCurrencyList = newListFromCodes( currencyCodeList ); }
  gConversionProfile = new VMC.ConversionProfile(VMC.currencyDB.generateDefaultSymbolTableFor(gSourceCurrencyList));

  var defaultEntry = new VMC.ConversionProfileTableEntry('Default', null , gConversionProfile); // null regexps (match all URLs)
  gConversionProfileTable = new VMC.ConversionProfileTable( [defaultEntry] );
};


  function checkTable(table,url,testDataList) {
    table.setActiveProfileEntryForUrl(url);
    var toEachTestData = function(testData) {
      var comment              = testData[0];
      var str                  = testData[1];
      var expectedCurrencyCode = testData[2];
      var commentStr = comment+ ' Symbol ['+str+'] is expected to be recognised as '+ expectedCurrencyCode;
 
      var actualCurrency = table.findCurrencyFromStr(str);
      var actualCurrencyCode = null;
      if (actualCurrency) { actualCurrencyCode = actualCurrency.code; }
 
      assertEquals(commentStr, expectedCurrencyCode, actualCurrencyCode );
    };
    VMC.applyFuncToArray(toEachTestData,testDataList);
  };

function testProductionConversionProfileTable() { //HACK: this default table should be produced by VMC, not here in the test code
  var prefManager = new VMC.PrefManager(); // no startpoint?
  var table = prefManager.getDefaultConversionTable();

  checkTable(table,'chrome://foobar',     [ [ 'chrome shoud not be converted',                 '$',   null ] ] );
  checkTable(table,'https://kbc.be/IPA/', [ [ 'https (as used by online banks) not converted', '$',   null ] ] );
  checkTable(table,'http://viewmycurrency.wordpress.com', [ [ 'VMC site not converted', '$',   null ] ] );



  checkTable(table,'http://www.google.com',
	     [ [ 'using default', '$',   'USD' ],
	       [ 'explicit CAD ', 'C$',  'CAD' ],
	       [ 'explicit USD ', 'US$', 'USD' ],
	       [ 'normal',        'GBP', 'GBP' ]
	       ] );

  checkTable(table,'http://www.google.ca',
	     [ [ 'Canadian!',     '$',   'CAD' ],
	       [ 'explicit CAD ', 'C$',  'CAD' ],
	       [ 'explicit USD ', 'US$', 'USD' ],
	       [ 'normal GBP',    'GBP', 'GBP' ]
	       ] );

}


function testConversionProfileTable() {

  function checkUrlsMatch(entry,testUrls) {
    var toEachUrl = function(url) { assert(entry.toString(false)+': match '+url, entry.matches(url)); };
    VMC.applyFuncToArray(toEachUrl,testUrls);
  }

  function checkUrlsDoNotMatch(entry,testUrls) {
    var toEachUrl = function(url) { assertFalse(entry.toString(false)+': should not match '+url, entry.matches(url)); };
    VMC.applyFuncToArray(toEachUrl,testUrls);
  }

  
  setSourceTo(['USD','CAD','GBP']); // set gConversionProfile


  var defaultProfile = gConversionProfile;
  var defaultEntry = new VMC.ConversionProfileTableEntry('Default', null, defaultProfile );
  checkUrlsMatch(defaultEntry, ['w', 'http://father', 'http://bbc.co.uk', 'about:blank'] ); 

  setSourceTo(['CAD','USD']); // set gConversionProfile
  var canadianProfile = gConversionProfile;
  var canadianEntry   = new VMC.ConversionProfileTableEntry('Canadaian', [ /\.ca($|\/)/, /\/ca\./, /canada/ ] , canadianProfile );
  checkUrlsMatch(canadianEntry,      [ 'http://canada411.yellowpages.ca',
				       'http://www.canadapost.ca/personal/tools/pg/manual/CPCPrices-e.asp#i10',
				       'http://www.canada.com/',
				       'http://ca.yahoo.com/' ] );
  checkUrlsDoNotMatch(canadianEntry, [ 'http://www.google.com ',
				       'about:blank',
				       'http://bbc.co.uk/' ] );

  var table = new VMC.ConversionProfileTable( [canadianEntry, defaultEntry] );


  checkTable(table,'http://www.google.com',
	     [ [ 'using default', '$',   'USD' ],
	       [ 'explicit CAD ', 'C$',  'CAD' ],
	       [ 'explicit USD ', 'US$', 'USD' ],
	       [ 'normal',        'GBP', 'GBP' ]
	       ] );

  checkTable(table,'http://www.google.ca',
	     [ [ 'Canadian!',     '$',   'CAD' ],
	       [ 'explicit CAD ', 'C$',  'CAD' ],
	       [ 'explicit USD ', 'US$', 'USD' ],
	       [ 'missing GBP',   'GBP', null ] //GBP isn't supported by my simple canadian profile
	       ] );
				       
}

function testSymbolTable() {
  if (!traceTest('Test new Symbol Table')) { return; }

  var USD = new VMC.Currency('United States','United States Dollar','USD', '$',null);
  var CAD = new VMC.Currency('Canada','Canadian Dollar','CAD',             '$',null);
  var GBP = new VMC.Currency('United Kingdom','United Kingdom Pound','GBP','quid',null);

  var usdSymbolList = new VMC.SymbolList(['$','US$','US $','USD','dollar']);
  assertFalse( 'not a USD: pound', usdSymbolList.matches('pound'));
  assert(      'USD: $',           usdSymbolList.matches('$'));

  var cadSymbolList = new VMC.SymbolList(['$','C$','CAD$','CAD','dollar']);
  assertFalse( 'not a CAD: pound', cadSymbolList.matches('pound'));
  assert(      'CAD: $',           cadSymbolList.matches('$'));
  assert(      'CAD: C$',          cadSymbolList.matches('C$'));

  var gbpSymbolList = new VMC.SymbolList(['GBP','\u00A3','quid']);
  assertFalse( 'not a GBP: dollar',gbpSymbolList.matches('dollar'));
  assert(      'GBP: $',           gbpSymbolList.matches('quid'));
  assert(      'GBP: C$',          gbpSymbolList.matches('\u00A3'));

  var symbolTable = new VMC.SymbolTable( [
				      new VMC.SymbolTableEntry( USD, usdSymbolList ),
				      new VMC.SymbolTableEntry( CAD, cadSymbolList ),
				      new VMC.SymbolTableEntry( GBP, gbpSymbolList ) ] );
  dump('symbolTable = '+symbolTable);
  assertEquals('not anything: foo', null, symbolTable.matches('foo')    );
  assertEquals('USD: $',            USD,  symbolTable.matches('$')      );
  assertEquals('USD: dollar',       USD,  symbolTable.matches('dollar') );
  assertEquals('CAD: C$',           CAD,  symbolTable.matches('C$')     );
  assertEquals('GBP: quid',         GBP,  symbolTable.matches('quid')   );
 
  symbolTable.move(1,0); // put CAD above USD
  dump('\n\n moved symbolTable = '+symbolTable);
  assertEquals('not anything: foo', null, symbolTable.matches('foo')    );
  assertEquals('CAD: $',            CAD,  symbolTable.matches('$')      );
  assertEquals('CAD: dollar',       CAD,  symbolTable.matches('dollar') );
  assertEquals('CAD: C$',           CAD,  symbolTable.matches('C$')     )
  assertEquals('USD: US$',          USD,  symbolTable.matches('US$')    );
  assertEquals('USD: USD',          USD,  symbolTable.matches('USD')    );
  assertEquals('GBP: quid',         GBP,  symbolTable.matches('quid')   );

  symbolTable.remove(0); //remove CAD
  dump('\n\n removed symbolTable = '+symbolTable);
  assertEquals('not anything: foo',  null, symbolTable.matches('foo')    );
  assertEquals('back to USD: $',     USD,  symbolTable.matches('$')      );
  assertEquals('back to USD: dollar',USD,  symbolTable.matches('dollar') );
  assertEquals('gone: C$',           null, symbolTable.matches('C$')     )
  assertEquals('USD: US$',           USD,  symbolTable.matches('US$')    );
  assertEquals('USD: USD',           USD,  symbolTable.matches('USD')    );
  assertEquals('GBP: quid',          GBP,  symbolTable.matches('quid')   );

  symbolTable.add(new VMC.SymbolTableEntry(CAD, cadSymbolList), 2); //put CAD back in last position
  assertEquals('not anything: foo', null, symbolTable.matches('foo')    );
  assertEquals('USD: $',            USD,  symbolTable.matches('$')      );
  assertEquals('USD: dollar',       USD,  symbolTable.matches('dollar') );
  assertEquals('CAD: C$',           CAD,  symbolTable.matches('C$')     );
  assertEquals('GBP: quid',         GBP,  symbolTable.matches('quid')   );

  symbolTable.createRegExps();

}

var gSourceCurrencyList;

//WILL: TODO: make a new page for all this web service stuff!
//WILL: TODO: Make a fixed rate webservice that can be used in a non-test release (test dir not normally included)
var LocalFixed  = function() {};
VMC.webServiceList.push(LocalFixed);
LocalFixed.getName = function() { return "LocalFixed"; };
LocalFixed.supportedCodes = []; // not used
LocalFixed.getUpdateUrl = function(fromCode,toCode) {  return 'chrome://viewmycurrency/content/tests/fixed_rates/'+fromCode+'2'+toCode; };
LocalFixed.getRateFromContent = VMC.webServiceList.getServiceFromName('WebServiceX').getRateFromContent; //re-use
//WILL: TODO: supported codes??




//   assertArraysEqual( [],  [],  'empty array' );
//   assertArraysEqual( [1],  [1],  'single item array' );
//   assertArraysEqual( [1,2,6,7,44,'','dfsdf',4,5],  [1,2,6,7,44,'','dfsdf',4,5],  'multi-type mutli-item array' );
//   return;

function checkAssociateNumbersWithSymbols(testStr, expectedNumberAssociation, comment) {
  var tokenStream = VMC.NumberConversion.prototype.tokeniseNumbers(testStr);
  var resultingNumberAssocation = VMC.NumberConversion.prototype.associateNumbers(tokenStream);
  assertArraysEqual(expectedNumberAssociation,resultingNumberAssocation,comment);
}

function checkTokenise(testStr,expectedTokenStream,comment) {
  debug('Testing: ['+testStr+']');
  var r=VMC.NumberConversion.prototype.tokeniseNumbers(testStr);
  var tokens=r.tokens;
  var types=r.types;

  var expectedTokens = expectedTokenStream.tokens;
  var expectedTypes  = expectedTokenStream.types;

  var resultStr=tokens.join('');
  assertEquals(comment+': joined tokens not original',  testStr,  resultStr);
  assertEquals(comment+': tokens and types not same length', tokens.length, types.length);

  var numObj;
  var numStr;
  var expected;
  for (var i=0;i<tokens.length;i++) {
    debug(types[i]+': ['+tokens[i]+']   Expected = '+expectedTypes[i]+': ['+expectedTokens[i]+']');
      
    if (types[i]==VMC.NumberConversion.prototype.NUM) { //do a special comparison
      assertEquals(comment+': token value wrong', expectedTokens[i],   new VMC.Number(tokens[i])._origNum);	
    } else {
      assertEquals(comment+': token value wrong', expectedTokens[i],   tokens[i]);	
    }

    assertEquals(comment+': token type wrong for token:'+tokens[i]+' type:', expectedTypes[i],  types[i]);
  }
}    

var TS='\v'; //TextSpace - to represent whitespace that has TEXT token type
function createTokens(values) {
  var knownSymbol = { 'US $':true, '$':true, 'R':true, '\xA3':true, 'GBP':true }; //WILL: TODO: extract all symbols from completeList
  var tokenStream = {};
  tokenStream.tokens=[];
  tokenStream.types=[];
  var value;
  var type;
  for (var i=0;i<values.length;++i) {
    value=values[i];
    type=null;
    if (value==TS) { type=VMC.NumberConversion.prototype.TEXT; value=value.replace(TS,' '); } //WILL: do I pass TEXPSP in now?
    else {
      if (typeof(value)=='number') { type=VMC.NumberConversion.prototype.NUM;    }
      else if (/\S/.exec(value)) {
	if (knownSymbol[value])    { type=VMC.NumberConversion.prototype.SYMBOL; }
	else                       { type=VMC.NumberConversion.prototype.TEXT;   }
      } else                       { type=VMC.NumberConversion.prototype.SPACE;  }
    }
    tokenStream.types[i]=type;
    tokenStream.tokens[i]=value;
  }
  return tokenStream;
}
// "\v\n\v\vGBP\v\n\xA3" currencyRightRegExp2 should match \xA3 not GBP!! --> but it should be done on first chunk. It has no RHS - there was no previous number WILL: TODO: move to firstIteration doc
//"^(\\s*)(dollars|pounds|bucks|pound|euros|cdn\\$|euro|au \\$|quid|us \\$|rub|zar|sbd|sek|pkr|eur|ggp|scr|ft\\.|twd|thb|pen|bnd|khr|chf|cad|ars|gip|lkr|aud|syp|sar|trl|yer|brl|tvd|uyu|gbp|cyp|dkk|ttd|dop|xcd|egp|svc|vnd|s\\/\\.|qar|shp|fkp|fjd|bsd|fr\\.|gyd|hkd|huf|bbd|c \\$|inr|bzd|rs\\.|srd|jmd|cny|ils|bob|usd|imp|jpy|cop|yen|clp|jep|kpw|crc|krw|lak|cup|lbp|lrd|mtl|bmd|mur|mxn|mnt|irr|nad|npr|ang|nzd|ngn|kyd|omr|awg|zwd|try|php|sgd|ft|c\\$|a\\$|ur|r\\$|rs|\xAB|\xA8|?|\xF9|r|\xDB|\xAC|\\$|\xA6|\xAE|\xA4|\xA3|\xAD|\xA1|\xA9|\x86|\xB1|\xA5|\x92|C|\xAA|\xFC)(\\s|\\d|=|$)"

//   // NB: remember that numbers must be passed as numbers not strings!
//   checkTokenise(
// 		'\v\n\v\vGBP\v\n\xA310 \v\v\n\xA310 \v\v\n\xA310 \v\v\n',
// 		createTokens( ['\v\n\v\vGBP\v\n','\xA3',10,' \v\v\n','\xA3',10,' \v\v\n','\xA3',10,' \v\v\n']),
// 		'multiple pounds not working');




// function testPreference() { //WILL: TODO: this interface got more complex, make new tests including a gui element
//   if (!traceTest('WILL: Test Preference class for Options dialog')) { return; }

//   var getVal = function() { return 1; }; // never changes
//   var writeVal = function() { }; // do nothing

//   var written;
//   var staticPref = new  VMC.Preference(getVal,writeVal);
//   written=staticPref.writeIfChanged();
//   assertFalse('static pref should not need a write',written);

//   getVal = function() { return Math.random() }; // always changes
//   var dynamicPref = new VMC.Preference(getVal,writeVal);
//   written=dynamicPref.writeIfChanged();
//   assert('dynamic pref should need a write',written);

//   var writtenByFunc = false;
//   writeVal = function() { writtenByFunc = true; }
//   dynamicPref = new VMC.Preference(getVal,writeVal);
//   written=dynamicPref.writeIfChanged();
//   assert('dynamic pref should need a write',written);
//   assert('dynamic pref should need a writeIfChanged',writtenByFunc);
// }
  

function testRegressionBigTableBug() {
  if (!traceTest('WILL: DOC this. this test led me to add end of element markers as well')) { return; }

  setSourceTo('COMPLETELIST'); //updates gConversionProfile and gSourceCurrencyList

  VMC.NumberConversion.prototype.currencyLeftRegExp2  = gConversionProfile.getLeftRE();
  VMC.NumberConversion.prototype.currencyRightRegExp2 = gConversionProfile.getRightRE();

  var test='\n\n1\n \n $ \n \n1\n\n';
  checkTokenise(test, createTokens( ['\n\n',1,'\n \n ','$',' \n \n',1,'\n\n'] ) ,'Regression: step 1 of previous big-table test');
  checkAssociateNumbersWithSymbols( test, [] , 'Regression: step 1 of previous big-table test. Boths 1s are 4 spaces from the $');

  //WILL: but does this test actually represent the HTML below?

  setSourceTo( ['USD','GBP','EUR'] );
  //var testHtml='\n\n1\n<sup>\n<span>$</span>\n</sup>\n1\n\n';
  var testHtml='1<sup>$</sup>1';
  var goldHtml='1<sup>$</sup>1'; //no change should be made - it's ambigious
  doConvertHtml(testHtml, 2, 'ZAR',  goldHtml, 'This used to be the test_big-table_bug_node-arrays-do-not-match. 2nd is converted - it should be 1st!',
		 false/*hideOriginal*/, 'END_NO_CHNG');
  //this is from getTestOfNodes --> var test='\v\n\n1\n\v\v\n\v\v$\v\n\v\n1\n\n'; Not the same as test above


}

function testInternalTokenise() {
  if (!traceTest('Test WILL: TODO: These are not really unit tests - they test internal functions')) { return; }

  setSourceTo('COMPLETELIST');
  VMC.NumberConversion.prototype.currencyLeftRegExp2  = gConversionProfile.getLeftRE();
  VMC.NumberConversion.prototype.currencyRightRegExp2 = gConversionProfile.getRightRE();
  
  checkTokenise('1',   createTokens( [1]         ),'no currency symbol');
  checkTokenise('1',   createTokens( [1]         ),'no currency symbol');
  checkTokenise('1$',  createTokens( [1,'$']     ),'USD symbol');
  checkTokenise('1R ', createTokens( [1,'R',' '] ),'ZAR symbol with trailing space');
  checkTokenise('1R',  createTokens( [1,'R']     ),'ZAR symbol was a bug: this regognised as text');


  checkTokenise('',createTokens( [] ),'empty string');

  checkTokenise('w',createTokens( ['w'] ),'one char word');
  checkTokenise('will',createTokens( ['will'] ),'one word');

  checkTokenise('1', createTokens( [1] ),                  'one char num');
  checkTokenise('11',createTokens( [11] ),                 'one num');
  checkTokenise('1234567890',createTokens( [1234567890] ), 'big num');

  checkTokenise('1,234',createTokens( [1234] ),            'num comma formatted');
  checkTokenise('1,234,567',createTokens( [1234567] ),     'big num comma formatted');

  checkTokenise('1,234,567.899',createTokens( [1234567.899] ),  'big num comma formatted + decimal');
  checkTokenise('1 234 567.899',createTokens( [1234567.899] ),  'big num space formatted + decimal');
  //WILL: TODO:  checkTokenise('1 234 5672',   createTokens( [1234567,2] ),    'malformatted space thousands. WILL: TODO: is there a better way to cope with this?');

  checkTokenise('1$',  createTokens( [1,'$'] ),                 'simple with currency symbol on right');
  checkTokenise(' 1$', createTokens( [' ',1, '$'] ),            'simple with currency symbol on right - space before');
  checkTokenise('1$ ', createTokens( [1,'$',' '] ),             'simple with currency symbol on right - space after ');

  checkTokenise('$1',  createTokens( ['$',1] ),                 'simple with currency symbol on left');
  checkTokenise(' $1', createTokens( [TS,'$',1]  ),             'simple with currency symbol on left - space before as TEXT not WHITESPACE');
  checkTokenise('$1 ', createTokens( ['$',1,' '] ),             'simple with currency symbol on left - space after ');

  checkTokenise('$ 1', createTokens( ['$',' ',1] ),             'simple with currency symbol on left - space between');

  checkTokenise('=$1', createTokens( ['=','$',1] ),             'simple with currency symbol - boundary char before - should be added to text');

  //WILL: TODO: check that whitespace tokens are only created for SYMBOL, SPACE, NUM or for NUM, SPACE, SYMBOL
  checkTokenise('wwwwwwwwww $10 wwwww 14.34 wwww 5 000 000  $ 144.2w', 
  		createTokens( ['wwwwwwwwww ','$',10,' wwwww ', 14.34, ' wwww ', 5000000, '  ','$',' ', 144.2,'w'] ), 'typical');
  //WILL: UNDONE this change: note the 144.2 is a string, not a number since it is attached to the 'w'.

  checkTokenise('111,111.11',       createTokens( [111111.11] ),        'regression test: xxx,xxx.xx not recognised');
  checkTokenise('US $111,111.11',   createTokens( ['US $',111111.11] ), 'regression test: US$ + big num comma formatted + decimal');

  checkAssociateNumbersWithSymbols( '$2.5' ,      [1,0] ,   'number at pos 1-->0 - simple case, symbol before');
  checkAssociateNumbersWithSymbols( '2.5$' ,      [0,1] ,   'number at pos 0-->1 - simple case, symbol after');
  checkAssociateNumbersWithSymbols( '2.5 $' ,     [0,2] , '0-->2       - whitespace between');
  checkAssociateNumbersWithSymbols( '2.5$ ' ,     [0,1] , '0-->1       - whitespace after');

  checkAssociateNumbersWithSymbols( '2.5$2.5' ,    [] , 'ambigious - both numbers 1 token away - no assocation');
  checkAssociateNumbersWithSymbols( '2.5 $ 2.5' ,  [] , 'ambigious - both numbers 2 token away - so no assocation');
  checkAssociateNumbersWithSymbols( '2.5  $ 2.5' , [4,2] , '4-->2 - whitespace before is bigger');
  checkAssociateNumbersWithSymbols( '2.5 $  2.5' , [0,2] , '0-->2 - whitespace before is bigger');

  checkAssociateNumbersWithSymbols( '$12   XXX $   12 $ 12' , [1,0] , '1-->0 (only unambigious)');
  checkAssociateNumbersWithSymbols( '6$6 $12$' , [] , 'both ambigious');

  checkAssociateNumbersWithSymbols( '$12',  [1,0] , 'simple test: $12');
  checkAssociateNumbersWithSymbols( '12 $', [0,2] , 'simple test: 12 $');

}
  


function testExchangeRateCacheLocalData() {
  if (!traceTest('Test the ExchangeRateCache with local data')) { return; }


  var sourceCurrencyList = newListFromCodes(['AUD','CAD','CHF','CNY','EUR','GBP','JPY','ZAR']);
  var targetCurrencyList = newListFromCodes(['USD']);

  var AUD = sourceCurrencyList.findFromCode('AUD');   assertValidRef('',AUD);
  var CAD = sourceCurrencyList.findFromCode('CAD');   assertValidRef('',CAD);
  var CHF = sourceCurrencyList.findFromCode('CHF');   assertValidRef('',CHF);
  var CNY = sourceCurrencyList.findFromCode('CNY');   assertValidRef('',CNY);
  var EUR = sourceCurrencyList.findFromCode('EUR');   assertValidRef('',EUR);
  var GBP = sourceCurrencyList.findFromCode('GBP');   assertValidRef('',GBP);
  var JPY = sourceCurrencyList.findFromCode('JPY');   assertValidRef('',JPY);
  var ZAR = sourceCurrencyList.findFromCode('ZAR');   assertValidRef('',ZAR);

  var USD = targetCurrencyList.findFromCode('USD');   assertValidRef('',USD);


  var dummyRate = 200.001;

  //WILL: this just downloads and checks one rate. Should consider extending to check a list of rates
  function checkDownloadFromCacheSuccess(sourceCurrencyList,targetCurrencyList,expectedRates,expectedErrors,webService) {
    var prefReadRateFunc  = function(o) { info('ReadRate:  read '+o+' from prefs (fixed rate '+dummyRate+')'); o.setRate(dummyRate);  };
    var prefWriteRateFunc = function(o) { info('WriteRate: write '+o+' (do nothing)'); };


    var cache = new VMC.ExchangeRateCache(prefReadRateFunc, prefWriteRateFunc, sourceCurrencyList, targetCurrencyList);
    assertValidRef('cache created', cache);
  
    cache.setAllRatesFromPrefs(); // reset rates to dummyRate (thanks to our hacked prefReadRateFunc)
    
    cache.setAllRatesToWebService(webService); 


    VMC.ExchangeRate.applyToEachRate(sourceCurrencyList,targetCurrencyList,
		    function(fromCurr,toCurr) {
		      assertEquals(fromCurr+'/'+toCurr+ ' should now be set to dummyRate', dummyRate , cache.getRate(fromCurr,toCurr));
		    } );
    
    //assertEquals(fromCurr+'/'+toCurr+' = initial silly rate', dummyRate , cache.getRate(fromCurr,toCurr));

    cache.updateFromWeb();
    waitForUpdateToFinish('Testing the ExchangeRateCache updates',cache.webUpdater);
    assertEquals('update errors', expectedErrors, cache.webUpdater._hadErrors);

    var rateIndex=0;
    VMC.ExchangeRate.applyToEachRate(sourceCurrencyList,targetCurrencyList,
		    function(fromCurr,toCurr) {
		      assertEquals(fromCurr+'/'+toCurr+ ' should now be set to rate', expectedRates[rateIndex] , cache.getRate(fromCurr,toCurr));
		      rateIndex++;
		    } );
  }


  checkDownloadFromCacheSuccess(sourceCurrencyList,targetCurrencyList,
				[ 0.7392, 0.8543, 0.7604, 0.1238, 1.1787, 1.7245, 0.0084, 0.1529], //expectedRates
				false, //expectedErrors
				VMC.webServiceList.getServiceFromName('LocalFixed'));


  var webService = new Object();
  webService.getRateFromContent=VMC.webServiceList.getServiceFromName('LocalFixed').getRateFromContent; // data is stored in the same data format
  webService.getUpdateUrl = function(fromCode,toCode) { return 'chrome://viewmycurrency/content/tests/bad_rates/'+fromCode+'2'+toCode; };

  checkDownloadFromCacheSuccess(sourceCurrencyList,targetCurrencyList,
 				[ 1.2, dummyRate,dummyRate,dummyRate,dummyRate,dummyRate,dummyRate,dummyRate], //expectedRates
				true,  //expectedErrors
				webService);

  // correponds the the nasty stuff in 
  // chrome/viewmycurrency/content/tests/bad_rates/*2USD
  // '0', '-1', '', 'bad rate', null, null, '-'

}

//****************** Test  WebServiceX       *****************************************
function testWebServiceX() {
  if (!traceTest('test the parsing routines for WebServiceX')) { return; }

  var xml;
  xml='<?xml version="1.0" encoding="utf-8"?>\n'+
      '<double xmlns="http://www.webserviceX.NET/">1.2087</double>';

  var getRateFunc = VMC.webServiceList.getServiceFromName('WebServiceX').getRateFromContent;
  assertEquals('parse normal',1.2087,getRateFunc(xml));
  assertCausesException('parsing empty',     function() { getRateFunc(null); } );
  assertCausesException('parsing malformed', function() { getRateFunc('the number is 3.5'); } );
}  

//************************************************************************************


function testGoogleSupportForAllCurrencies() {
  warn('WILL: TODO: put google test in seperate file - SKIPPING');
  return;
}
function DONOTTESTGoogleSupportForAllCurrencies() {
  if (!traceTest('Test the complete currency list with Google')) { return; }  

  var updater = new VMC.WebUpdater();
  var webServiceUrl='http://www.google.com/search?q=1+FROM_CODE+in+TO_CODE';
  var currencyList = gCompleteCurrencyList;
 
 for (var i=0;i<currencyList.items.length;i++) {


    var url=webServiceUrl.replace('FROM_CODE',currencyList.items[i].code);
    url=url.replace('TO_CODE','EUR');
    if (updater.test(url, VMC.google_parseRateFromContent)) {
      info(currencyList.items[i].code+': OK');
    } else {
      info(currencyList.items[i].code+': FAILED');
    }
  }
}


//****************** Test       Google       *****************************************

function testWebServiceGoogle() {
  if (!traceTest('test the parsing routines for Google')) { return; }
  
  var googleXml;
  var items = [ new Item('one', 'http://www.google.com/search?q=1+usd+in+eur'),
		new Item('testCOPbug','http://www.google.be/search?q=1+usd+in+cop') ];
  items[0].expectedRange = [0.5 , 2];    // 1 USD could be 0.5 EUR to 2 EURs
  items[1].expectedRange = [1000, 3000]; // 1 USD could be 1000 to 3000 COPs
  
  var setFromXmlCallback = function(item,xml)      { item.googleXml=xml; };
  var setFailureCallback = function(item,errorStr) { alert('got failure'+errorStr); };

  var updater = new VMC.WebUpdater();
  //WILL: register observers?
  updater.start(items,setFromXmlCallback,setFailureCallback);

  //WILL: is not busy for local items that fail. Now it should be - update runs in new thread
  assertTrue('webUpdater has started',updater.isBusy );
  waitForUpdateToFinish('checking that google finished',updater);

  for (var i=0;i<items.length;i++) {
    var  googleXml = items[i].googleXml;
    dump('xml='+googleXml);
    var rateStr;
    try {
      rateStr=VMC.webServiceList.getServiceFromName('Google').getRateFromContent(googleXml);
    } catch (e) {
      assert('got exception '+e,false);
    }
    var rate=parseFloat(rateStr);
    var expectedLow  = items[i].expectedRange[0];
    var expectedHigh = items[i].expectedRange[1];
    assertTrue('Item ' +items[i]+' sensible rate from google '+expectedLow+' to '+expectedHigh+', got '+rate+' from ['+rateStr+']',
	       ( (rate>=expectedLow) && (rate<=expectedHigh) ) );
    info('got sensible rate: '+rate+' from google');
  }

  //  assertEquals('parse normal','1.2087',VMC.webservicex_parseRateFromContent(xml));
  //assertCausesException('parsing empty', function() { VMC.webservicex_parseRateFromContent(null); } );
  //assertCausesException('parsing malformed', function() { VMC.webservicex_parseRateFromContent('the number is 3.5'); } );
}  

//************************************************************************************


function testLocale2Currency() {
  if (!traceTest('Test that every supported locale has a currency')) { return; }
  var currencyDB = new VMC.CurrencyDataBase(true);  //includeNotSupported
  assertValidRef('currencyDB',currencyDB);

  var countriesWithoutCurrencies="";
  var cc2name=currencyDB.countryCode2CountryName;
  for (ccode in cc2name) {
    //debug('code=['+code+'] country=['+cc2name[code]+']');
    var country=cc2name[ccode];
    var currencyList = currencyDB.createListFromCountry(country,true); // failSilently
    if (VMC.CurrencyList.prototype.isValid(currencyList,true)) { //failSilently
      //info('code=['+ccode+'] country=['+country+'] = '+currencyList);
    } else {
      //warn('code=['+ccode+'] country=['+country+'] has no currency');
      countriesWithoutCurrencies += ccode +',';
    }
  }
  var expectedCountriesWithoutCurrencies='AQ,AS,AX,BV,CX,EH,FO,GF,GL,GP,GS,GU,HM,MP,MQ,NF,NU,PM,PN,PR,RE,SJ,ST,TF,TK,TL,TV,UM,VI,YT,';
  
  //info('countriesWithoutCurrencies='+countriesWithoutCurrencies);
  assertEquals('all supported countries have a currency',expectedCountriesWithoutCurrencies,countriesWithoutCurrencies);


  // List of intl.accept_languages from http://lxr.mozilla.org/l10n/search?string=accept_languages
  var locales = [
	     'ca',    'ca, en-us, en', 
	     'bg',    'bg, en-us, en', 
	     'ar',    'ar-jo, ar, en-us, en', 
	     'cs',    'cs, en-us, en', 
	     'de',    'de-de, de, en-us, en', 
	     'ru',    'ru-ru, ru, en-us, en', 
	     'sl',    'sl, en-gb, en', 
	     'fr',    'fr, fr-fr, en-us, en', 
	     'el',    'el-gr, el, en-us, en',  //WILL: TODO: file BUG: this used to say en-us DOT
	     'hu',    'hu-hu, hu, en-us, en', 
	     'it',    'it-it, it, en-us, en', 
	     'pl',    'pl, en-us, en', 
	     'nl',    'nl, en-us, en', 
	     'fi',    'fi, en', 
	     'he',    'he-il, he, en-US, en', 
	     'ja',    'ja, en-us, en', 
	     'ro',    'ro, en-us, en-gb, en', 
	     'eu',    'eu, en-us, en', 
	     'da',    'en-us, en',            //WILL: TODO: file BUG: where is da?
	     'mk',    'mk, en', 
	     'sq',    'sq-AL, sq, en-us, en', 
	     'sk',    'sk, cs, en-us, en', 
	     'ko',    'ko-kr, ko, en-us, en', 
	     'mn',    'mn, en-us, en', 
	     'tr',    'tr-TR, tr, en-us, en', 
	     'en-GB', 'en-gb, en', 
	     'nb-NO', 'nb, no, nn, en-us, en', 
	     'es-AR', 'es-ar, es, en-us, en', 
	     'ga-IE', 'ga, en-ie, en-us, en', 
	     'sv-SE', 'sv, en-us, en', 
	     'gu-IN', 'gu-in, gu, en-us, en', 
	     'es-ES', 'es-es, es, en-us, en', 
	     'pa-IN', 'pa, pa-in, en-us, en', 
	     'zh-CN', 'zh-cn,zh', 
	     'zh-TW', 'zh-tw, en-us, en', 
	     'hy-AM', 'hy-AM, hy, ru-RU,ru, en-EN,en', 
	     'fy-NL', 'fy, nl, en-us, en', 
	     'pt-BR', 'pt-br, pt, en-us, en', 
	     'nn-NO', 'nn, no, nb, en-us, en', 
	     'ja-JP-mac',    'ja, en-us, en'
	     ];

  for (var i=0;i<locales.length;i+=2) {
    var locale = locales[i];
    var accept_languages = locales[i+1];
    var localeCurrencyList = currencyDB.createListFromLanguages(accept_languages);
    var comment='Browser lcoale = '+locale+' accepts anguages '+accept_languages+' currencies = '+localeCurrencyList;
    info(comment);
    VMC.CurrencyList.prototype.isValid(localeCurrencyList); // will throw exception if not valid
    //WILL: TODO: I don't actually check if it's the right currencies
  }


  var accept_languages = nsPreferences.getLocalizedUnicharPref('intl.accept_languages');
  var localeCurrencyList = currencyDB.createListFromLanguages(accept_languages);
  var comment='This browser\'s languges are '+accept_languages+' currencies = '+localeCurrencyList;
  info(comment);
  VMC.CurrencyList.prototype.isValid(localeCurrencyList); // will throw exception if not valid
  //WILL: TODO: I don't actually check if it's the right currencies

  // No longer used since we base choice on accept-languages
//   var myLocale=navigator.language;
//   var myCurrency=completeCurrencyList.findFromLocale(myLocale);
//   assertValidRef("this browser's locale has a currency",myCurrency);
//   //info("This browser's locale is "+myLocale+" currency="+myCurrency);


  // bug found by Marco Guadagnini has "Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.0.1) Gecko/20060111 Firefox/1.5.0.1"
  // test all the Firefox locales from http://lxr.mozilla.org/l10n/find?string=browser-region%2Fregion.properties
  var locales = [];
  locales.push('ca','bg','ar','cs','de','ru','sl','fr','el','hu','it','pl','nl','fi','he','ja','ro','eu','da','mk','sq','tr','ko','mn','sk'); //abc
  locales.push('nn-NO','pt-BR','fy-NL','hy-AM','zh-TW','zh-CN','es-ES','pa-IN','en-GB','gu-IN','sv-SE','ga-IE','es-AR','nb-NO'); // abc-XY 
  locales.push('ja-JP-mac'); // abc-XY-x: 

  for (var i=0;i<locales.length;i++) {
    var currencyList=currencyDB.findFromLocale(locales[i]);
    VMC.CurrencyList.prototype.isValid(currencyList); // will throw exception if not valid
  }

}



//****************** Test  ExchangeRateCache *****************************************
function testExchangeRateCache() {
  if (!traceTest('Test the ExchangeRateCache with live data from exteranl webservice')) { return; }

  gSourceCurrencyList = newListFromCodes(['GBP']);
  var targetCurrencyList = newListFromCodes(['EUR']);


  var GBP = gSourceCurrencyList.findFromCode('GBP');
  var EUR = targetCurrencyList.findFromCode('EUR');
  assertValidRef('',GBP);
  assertValidRef('',EUR);
  var dummyRate = 200.001;
  var prefReadRateFunc  = function(o) { info('ReadRate:  read '+o+' from prefs (fixed rate '+dummyRate+')'); o.setRate(dummyRate);  };

  var prefWriteRateFunc = function(o) { info('WriteRate: write '+o+' (do nothing)'); };

  
  var cache = new VMC.ExchangeRateCache(prefReadRateFunc, prefWriteRateFunc, gSourceCurrencyList, targetCurrencyList);
  assertValidRef('cache created', cache);
  
  var rateObj = cache.getRateObj(GBP,EUR);
  assertValidRef('rateObj exists', rateObj);

  assertValidRef('has an updateURL',rateObj.getUpdateUrl());

  assertEquals('GBP/EUR=fixed rate', dummyRate , cache.getRate(GBP,EUR));
  //This rate doesn't exist - assertEquals('EUR/GBP=fixed rate', dummyRate , cache.getRate(EUR,GBP));
  
  // The rates will all be out of date, because updateFromString() was never called from the real PrefManager.readRate()
  cache.updateFromWeb();
  waitForUpdateToFinish('test that webupdate finishes ',cache.webUpdater);
  assertFalse('update without errors', cache.webUpdater._hadErrors);

  assert('web update has changed rate', rateObj.getRate() != dummyRate);

  //WILL: TODO: how to check that this get written to prefs
}



//****************** Test  ExchangeRateCache webservices list *****************************************
function testExchangeRateCacheWebServicesList() {
  warn('WILL: skipping Google - do not want a blacklisting!');
  return;
}
function DONOTTESTExchangeRateCacheWebServicesList() {
  if (!traceTest('Test the ExchangeRateCache List OK Webservices')) { return; }

  gSourceCurrencyList = newListFromCodes(['USD','YER']); // WILL: Don't do them all. Google will backlist you! gCompleteCurrencyList;
  var targetCurrencyList = newListFromCodes(['EUR']);

 
  var dummyRate = 200.001;
  var prefReadRateFunc  = function(o) { info('ReadRate:  read '+o+' from prefs (fixed rate '+dummyRate+')'); o.setRate(dummyRate);  };
  var prefWriteRateFunc = function(o) { info('WriteRate: write '+o+' (do nothing)'); };

  var cache = new VMC.ExchangeRateCache(prefReadRateFunc, prefWriteRateFunc, gSourceCurrencyList, targetCurrencyList);
  assertValidRef('cache created', cache);

  cache.listOKwebServices(null,[VMC.webServiceList.getServiceFromName('Google')]);

  warn('WILL: TODO: verify that Google gets some and not others. Make a test for the other services ');
}

//****************** Test all currencies have webservices *****************************************
function testAllCurrenciesHaveWebService() {
  if (!traceTest('Test all currencies have webservices')) { return; }

  var sourceCurrencyList = VMC.currencyDB.getCompleteList();
  var targetCurrencyList = newListFromCodes(['EUR']);

  var dummyRate = 200.001;
  var prefReadRateFunc  = function(o) { info('ReadRate:  read '+o+' from prefs (fixed rate '+dummyRate+')'); o.setRate(dummyRate);  };
  var prefWriteRateFunc = function(o) { info('WriteRate: write '+o+' (do nothing)'); };

  var cache = new VMC.ExchangeRateCache(prefReadRateFunc, prefWriteRateFunc, sourceCurrencyList, targetCurrencyList);
  assertValidRef('cache created', cache);

  var allRatesHaveWebService=cache.setMissingRateWebServices();
  assert('allRatesHaveWebService',allRatesHaveWebService);

  //cache.listOKwebServices(null,[VMC.webServiceList.getServiceFromName('Google')]);


}


//****************** Test WebUpdater *******************************************
var WEB_TIMEOUT = 10; // timeout web updates after 10s

function Item(aTitle,aUrl) {
  this.toString           = function() { return "Test item '"+aTitle+"'"; };
  this.webService = VMC.webServiceList.getServiceFromName("LocalFixed"); //WILL: This isn't actually used?
  this.getUpdateUrl       = function() { return aUrl; }; // because getUpdateUrl doesn't use the webservice //WILL: seems ugly
}

//WILL: is this really local?
function testWebUpdaterLocally() {
  if (!traceTest('Test the WebUpdater with local data')) { return; }
  var url;
  var items;
  // test loadXML success on dummy data
  url="chrome://viewmycurrency/content/tests/dummyRates.txt";
  //  items = [ new Item('one',url) ];
  items = [ new Item('one',url) , new Item('two',url) , new Item('tree',url) ];
  webUpdateItemsExpectSuccess('three item with valid local chrome address',items);

  // test loadXML success on remote data
  url="http://www.google.com"; //
  //  items = [ new Item('one',url) ];
  items = [ new Item('one',url) , new Item('two',url) , new Item('tree',url) ];
  webUpdateItemsExpectSuccess('three item with valid http address',items);


}

function testWebUpdaterBadURLs() {
  if (!traceTest('Test the WebUpdater with bad URLs')) { return; }

  var url;
  url="chrome://viewmycurrency/content/tests/missingFile.txt";

  var items;
  items = [ new Item('one',url) ];
  webUpdateItemsExpectFailure('one item with bad url',items);

  items = [ new Item('one',url) , new Item('two',url) , new Item('tree',url) ];
  webUpdateItemsExpectFailure('three items with bad url',items);

  url="http://www.a.com";
  items = [ new Item('one',url) ];
  webUpdateItemsExpectFailure('one item with bad remote url',items); //WILL: TODO: This failed with timeout when I updated to Firefix 1.5rc1 but the 2nd time it was fine. Is there a cache problem?

}

function waitForUpdateToFinish(comment,updater) {
  var i=0;
  while (updater.isBusy && i<WEB_TIMEOUT) {
    debug('i='+i+' isBusy='+updater.isBusy+' items left='+updater._itemsToDo);
    WILLTEST.sleep(1000); 
    ++i;
  }
  assertFalse(comment+' webUpdater has not finished after '+i+' iterations',updater.isBusy );
}
function webUpdateItems(comment,items,itemLoaded,itemErrors) {
  var setFromXmlCallback = function(item,xml)      { itemLoaded[item] = true; };
  var setFailureCallback = function(item,errorStr) { itemErrors[item] = errorStr; };
  var updater = new VMC.WebUpdater();
  //WILL: register observers?
  updater.start(items,setFromXmlCallback,setFailureCallback);

  //WILL: is not busy for local items that fail. Now it should be - update runs in new thread
  assertTrue('webUpdater has started',updater.isBusy );
  waitForUpdateToFinish(comment,updater);
}

function webUpdateItemsExpectFailure(comment,items) {
  var itemLoaded = {};
  var itemErrors = {};
  webUpdateItems(comment,items,itemLoaded,itemErrors);
  // check that the items DID NOT load
  var item;
  for (var i=0;i<items.length; i++) {
    item=items[i];
    assert(comment+' Item '+item+' with bad URL should have failed to load', !itemLoaded[item] );
    assertValidRef( comment+' Item '+item+' with bad URL should have returned error', itemErrors[item] );
    info(item+' not loaded as expected');
  }
}

function webUpdateItemsExpectSuccess(comment,items) {
  var itemLoaded = {};
  var itemErrors = {};
  webUpdateItems(comment,items,itemLoaded,itemErrors);
  // check that the items DID load
  var item;
  var i;
  for (i=0;i<items.length; i++) {
    item=items[i];
    assertValidRef( comment+' Item '+item+' with good URL should have loaded', itemLoaded[item] );
    assert(comment+' Item '+item+' with good URL should not have returned error', !itemErrors[item] );
    info(item+' loaded as expected');
  }
}

//****************** Test VMC.PrefManager *************************************
function testPrefManager() { //WILL: TODO: I should really develop these tests, especially if I do version migration!
  if (!traceTest('Test the preference manager')) { return; }
  var result;
  var startpoint='extensions.viewmycurrency_test.';
  var prefM;

  // The real pref manager
  prefM = new VMC.PrefManager(startpoint).init();
  prefM._ps.deleteBranch(''); // delete all prefs in the test startpoint

  result = prefM.determineStatusOfExistingPrefs();
  assertEquals('test prefs are empty', VMC.PrefManager.stateVirgin, result );
  prefM.storeInitialPrefs( newListFromCodes( ['USD'] ) );
  result = prefM.determineStatusOfExistingPrefs();
  assertEquals('test prefs are there', VMC.PrefManager.stateValidPrefsPresent, result );

  prefM._ps.deleteBranch(''); 
  result = prefM.determineStatusOfExistingPrefs();
  assertEquals('test prefs are empty again', VMC.PrefManager.stateVirgin, result );


}



//****************** Test VMC.HtmlConversion WILL: TODO: should be in seperate file *******************************
function doConvertHtml(origHtml, rate, targetCode,  expectedHtml, comment, hideOriginal, expectedStatus) {
  if (hideOriginal===undefined) { hideOriginal = true; }
  checkConvertHtml(origHtml, null /*defaultSourceCurr*/, rate, targetCode,  expectedHtml, hideOriginal, comment, expectedStatus);
}

function checkConvertHtml(origHtml, defaultSourceCurr, rate, targetCode,  expectedHtml, hideOriginal, comment, expectedStatus) {
  if (!comment) { comment='NO COMMENT'; }
  comment += ': conversion of '+origHtml+' (target='+targetCode+' rate='+rate+')';
  
  var getRateFunc  = function() { return rate; };
  var status ='';
  var notifyFunc = function(s) { status += s; };

  var targetCurrency = VMC.currencyDB.getCompleteList().findFromCode(targetCode);
  assertValidRef('targetCurrency found for '+comment,targetCurrency);
  
  var newWin = open('about:blank','ViewMyCurrent Unit Test','width=30,height=30');
  assertValidRef('You must add site "viewmycurrency" to the Allowed Sites - Popups list',newWin);
  var doc = newWin.document;
  doc.body.innerHTML=origHtml;
  VMC.HtmlConversion.highlightChanges = false; //disable adding extra style
  VMC.HtmlConversion.changePageToTargetCurrencyUsingNodes(doc,targetCurrency, hideOriginal, gConversionProfileTable, getRateFunc, notifyFunc);
  WILLTEST.sleep(10);  //changePage is now async
  var actualHtml = doc.body.innerHTML;
  //info('origHtml = <pre>'+origHtml+'</pre>');
  //info('doc body='+actualHtml);
  newWin.close();
  
  assertEquals(comment, expectedHtml, actualHtml);
  assertEquals(comment+' (status) ',expectedStatus,status);
}

function testHtmlConversion() {

  if (!traceTest('Test the VMC.HtmlConversion routines')) { return; }
  var comment;

  setSourceTo( ['USD','GBP','EUR'] );


  var testHtml = 'Current bid:<b>US $160,100.00 </b>';
  var goldHtml = 'Current bid:<b>R320,200 (US $160,100.00) </b>'; //rounded
  doConvertHtml(testHtml, 2, 'ZAR',  goldHtml, 'Regression test: decimal point not included', false/*hideOriginal*/, 'END_CHANGES');
  
  testHtml='\n\n1\n<sup>\n<span>$</span>\n</sup>\n1\n\n';
  goldHtml=testHtml; //no conversion
  doConvertHtml(testHtml, 2, 'ZAR',  goldHtml, 'Test element end marker - preivously this was closer to the 2nd 1. Now it is ambigious. OK',
		false/*hideOriginal*/, 'END_NO_CHNG'); //FF1.5 - This test fails on FF 1.0.7  


  testHtml='normal conversoin: $100';
  goldHtml='normal conversoin: R200';
  doConvertHtml(testHtml, 2, 'ZAR',  goldHtml, 'Comments are not converted', true/*hideOriginal*/, 'END_CHANGES'); 

  testHtml='a comment followed by a number should not be converted: <!-- $ -->100';
  goldHtml=testHtml; //no conversion.
  doConvertHtml(testHtml, 2, 'ZAR',  goldHtml, 'Comments are not converted', false/*hideOriginal*/, 'END_NO_CHNG'); 

  testHtml='<script> alert(\'200$\');</script><div onclick="alert(\'100$\');">hello div</div>';
  goldHtml=testHtml; //no conversion.
  doConvertHtml(testHtml, 2, 'ZAR',  goldHtml, 'Scripts + handlers are not converted', false/*hideOriginal*/, 'END_NO_CHNG'); 


  // Test ignore PRE
  testHtml='<pre>$0="silly.pl"</pre>'; //WILL: Firefox seems to convert all html to lowercase, so we cannot use <PRE> here
  goldHtml=testHtml; //no conversion.
  doConvertHtml(testHtml, 2, 'ZAR',  goldHtml, 'CODE/PRE block not converted', false/*hideOriginal*/, 'END_NO_CHNG'); 

  // Test ignore CODE
  testHtml='<code>print "$100 dollars"</code>';
  goldHtml=testHtml; //no conversion.
  doConvertHtml(testHtml, 2, 'ZAR',  goldHtml, 'CODE/PRE block not converted, even if it contains currency', false/*hideOriginal*/, 'END_NO_CHNG'); 

  // Test ignore PRE
  testHtml='This script costs $100<code>if($1){doit();}</code>';
  goldHtml='This script costs R200<code>if($1){doit();}</code>';
  doConvertHtml(testHtml, 2, 'ZAR',  goldHtml, 'first part converted, code is not', true/*hideOriginal*/, 'END_CHANGES'); 


}

//****************** Test VMC.CurrencyConvertStr*******************************

function testCurrencyConvertStr() {
  if (!traceTest('Test the VMC.CurrencyConvertStr routine')) { return; }
  var comment;

  var NO_CHANGE = 'NO_CHANGE';

  function doDefaultConvertStr(origText, defaultSourceCurrCode, rate,   targetCode,  expectedStr, comment, hideOriginal) {
    if (hideOriginal===undefined) { hideOriginal = false; } //default to showing original value too
    assertValidRef('defaultSourceCurrCode required',defaultSourceCurrCode);
    // Used in PopupConvert - if a number is found without a currency symbol, then use the default currency
    var defaultSourceCurr = gSourceCurrencyList.findFromCode(defaultSourceCurrCode);
    assertValidRef('not a valid default source currency '+defaultSourceCurrCode,   defaultSourceCurr);
    comment += '(defaultSourceCurrCode='+defaultSourceCurrCode+')'; 

    checkConvertStr(origText, defaultSourceCurr, rate, targetCode,  expectedStr, hideOriginal, comment);
  }
    
  function doConvertStr(origText, rate, targetCode,  expectedStr, comment, hideOriginal, turnOffRounding) {
    var oldPerformRounding = VMC.Number.prototype.performRounding;
    if (turnOffRounding) { VMC.Number.prototype.performRounding = false; }

    if (hideOriginal===undefined) { hideOriginal = true; } //default to HIDING original value
    checkConvertStr(origText, null /*defaultSourceCurr*/, rate, targetCode,  expectedStr, hideOriginal, comment);
    
    VMC.Number.prototype.performRounding = oldPerformRounding;

  }

  function checkConvertStr(origText, defaultSourceCurr, rate, targetCode,  expectedStr, hideOriginal, comment) {
    if (!comment) { comment='NO COMMENT'; }
    if (expectedStr == NO_CHANGE) { expectedStr = origText; }

    comment += ': conversion of '+origText+' (target='+targetCode+' rate='+rate+')';

    var getRateFunc  = function() { return rate; };

    var targetCurrency = VMC.currencyDB.getCompleteList().findFromCode(targetCode);
    assertValidRef('targetCurrency found for '+comment,targetCurrency);
    
    var newText = VMC.NumberConversion.prototype.currencyConvertStr(origText, targetCurrency, gConversionProfile, getRateFunc, hideOriginal, defaultSourceCurr);
    if (newText===null) { newText=origText; } // no changes were made
    assertEquals(comment, expectedStr, newText);
  }

  // test to BEF
  setSourceTo( ['USD'] );
  doConvertStr('1$', 2,  'BEF', '2.00BFr', 'USD to BEF'); 
  doConvertStr('$1', 2,  'BEF', 'BFr2.00', 'USD to BEF'); 


  // test from BEF
  setSourceTo( ['BEF'] );
  doConvertStr('1BEF',  2,  'USD', '2.00$', 'BEF  (BEF) to USD'); 
  doConvertStr('1BFr',  2,  'USD', '2.00$', 'BFr  (BEF) to USD'); 
  doConvertStr('1BFr.', 2,  'USD', '2.00$', 'BFr. (BEF) to USD'); 
  //doConvertStr('$1', 2,  'BEF', 'BFr2.00', 'USD to BEF'); 


  setSourceTo( ['USD','GBP','EUR'] );

  //src	rate	result	rounded
  //0.1	0.83	0.083	 R0.08 
  //0.1	0.083	0.0083	 R0.01 --> WILL: TODO

  // test rounding bug:
  // doDefaultConvertStr('0.1',   'USD',  0.83,  'ZAR', 'R0.08',   '0.1*0.83=0.083 --> 0.08 rounded',  true/*hideOriginal*/);
  //numberStrJavascript = "0.083";
  //newNumberOfSigFigs = 2;
  //newMaxDecimals = -10;
  //numberStr=VMC.SignificantFigures.prototype.displaySigFigs(numberStrJavascript,newNumberOfSigFigs,newMaxDecimals,false); //scientific notation = false 
  // --> returns 0.083 but I had expected 0.08.
  comment="TODO: I need to change my algorithm to accept that leading decimal zeros don't count as significant figures";
  // TODO: Investigate the use of Number.toPrecision() and Number.toFixed();
  //WILL_NumberConversion.js:313
  //WILL: TODO: linked to Simons $0.01?
  info(comment);
  doDefaultConvertStr('0.01',  'USD',  0.83,  'ZAR', 'R0.00',    comment,true/*hideOriginal*/);
  doDefaultConvertStr('0.010', 'USD',  0.83,  'ZAR', 'R0.00830', 'OK3',  true/*hideOriginal*/);

  // test special Euro exceptions
  doConvertStr( 'EUR 2004',  2,  'USD', '$ 4,008', 'EUR symbol is always treated as currency');
  doConvertStr('EURO 2004',  2,  'USD', NO_CHANGE, 'Special exception, this might be the name of a competition');
  doConvertStr('2004 EURO',  2,  'USD', NO_CHANGE, 'I do not convert this either, maybe I should?'); //WILL: TODO
  doConvertStr('EURO25',     2,  'USD', NO_CHANGE, 'The 25 states of the EU, not a price');
  doConvertStr('25EURO',     2,  'USD', NO_CHANGE, 'I do not convert this either, maybe I should?'); //WILL: TODO
  doConvertStr('EURO80',     2,  'USD', '$160',    'Covert numbers between 30 and 80, they are probably prices.');
  doConvertStr('EURO30',     2,  'USD', '$60',     'Covert numbers between 30 and 80, they are probably prices.');
  doConvertStr('EURO29',     2,  'USD', NO_CHANGE, 'No convert, possible name');
  doConvertStr('EURO00',     2,  'USD', NO_CHANGE, 'No convert, possible name');
  doConvertStr('EURO29',     2,  'USD', NO_CHANGE, 'No convert, possible name');



  // test option to chose which item goes in the brackets
  doConvertStr('$1.00', 2,  'ZAR', 'R2.00 ($1.00)', 'source goes in brackets by default', false);

  VMC.NumberConversion.prototype.targetInBrackets = true;

  doConvertStr('$1.00', 2,  'ZAR', '$1.00 (R2.00)', 'put target in brackets, item in brackets always comes second', false);
  doConvertStr('1.00$', 2,  'ZAR', '1.00$ (2.00R)', 'order of symbol and number is respected and makes no difference to bracket order', false);

  var orig     = 'One hamster costs $'+nbsp+'1000000.99 profit is 100$';
//var bugresult= 'One hamster costs $'+nbsp+'1000000.99'+nbsp+' (R1,000,000.99) profit is 100$ (100R)';  // There was an extra nbsp
  var expected = 'One hamster costs $'+nbsp+'1000000.99'+     ' (R1,000,000.99) profit is 100$ (100R)';

  warn('skipping extra nbsp bug');  //HACK: doConvertStr(orig, 1,  'ZAR', expected, 'test bug: extra nbsp was insserted', false);


  VMC.NumberConversion.prototype.targetInBrackets = false; // restore to default

  // test force order
  doConvertStr('$1.00', 2,  'ZAR', 'R2.00', 'Normal order (RESPECT by default)');
  doConvertStr('1.00$', 2,  'ZAR', '2.00R', 'Normal order (RESPECT by default)');

  VMC.NumberConversion.prototype.forceSymbolPosition='LAST';
  doConvertStr('$1.00', 2,  'ZAR', '2.00R', 'Symbol forced last');

  VMC.NumberConversion.prototype.forceSymbolPosition='RESPECT';
  doConvertStr('$1.00', 2,  'ZAR', 'R2.00', 'Back to Normal order');
  doConvertStr('1.00$', 2,  'ZAR', '2.00R', 'Back to Normal order');

  VMC.NumberConversion.prototype.forceSymbolPosition='FIRST';
  doConvertStr('1.00$', 2,  'ZAR', 'R2.00', 'Number forced after');

  VMC.NumberConversion.prototype.forceSymbolPosition='RESPECT';
  doConvertStr('$1.00', 2,  'ZAR', 'R2.00', 'Back to Normal order 2');
  doConvertStr('1.00$', 2,  'ZAR', '2.00R', 'Back to Normal order 2');

  VMC.NumberConversion.prototype.forceSpaceBetweenNumberAndSymbol=true;

  doConvertStr('$1.00',  2,  'ZAR', 'R'+nbsp+'2.00', 'force space between 1');
  doConvertStr('1.00$',  2,  'ZAR', '2.00'+nbsp+'R', 'force space between 2');
  doConvertStr('$ 1.00', 2,  'ZAR', 'R 2.00', 'force space between - no extra space 1');
  doConvertStr('1.00 $', 2,  'ZAR', '2.00 R', 'force space between - no extra space 2');

  VMC.NumberConversion.prototype.forceSpaceBetweenNumberAndSymbol=false;
  doConvertStr('$1.00', 2,  'ZAR', 'R2.00', 'Back to Normal space');




  doDefaultConvertStr('Set at $100.', 'USD',  10,  'ZAR', 'Set at R1,000.'  , 'The trailing decimal should be treated as a boundary char',true/*hideOriginal*/); //WILL: this probably should use the simple doConvertStr since there is a $ sign in the string. It doesn't need the default?


  doDefaultConvertStr('will1024', 'USD',  10,  'ZAR', NO_CHANGE  , 'There should be no change, number is ambigious');

  doConvertStr('US $111,111.11', 2,  'ZAR', 'R222,222',    'Rounded');
  doConvertStr('US $111,111.11', 2,  'ZAR', 'R222,222.22', 'Regression test: decimal part not included', true, true); //hideOriginal, turnOffRounding
  doConvertStr('US $160,100.00', 2,  'ZAR', 'R320,200.00 (US $160,100.00)', 'Regression test: showing original causes problems with deciaml point',
	       false, true);

  //it's correct that there is no space between currency and number
  doConvertStr('(US $1.00', 2,  'ZAR', '(R2.00', 'Regression test: bracket as word boundary');
  doConvertStr('111$/room', 2,  'ZAR', '222R/room'  , '/ should be treated as valid boundary char');


  doConvertStr('\xA310 \xA310', 10,     'ZAR', 'R100 R100', 'Regression test: multiple pounds bug');

  doDefaultConvertStr('4', 'USD',  10,  'ZAR', 'R40 ($4)'  , '4 --> R40 using USD as default currency');

  doConvertStr('US $4', 10,     'ZAR', 'R40'); // Regression test: 'US $' recognised only as '$' 

  var dummyRate;

  var doUKpounds = function() {
    doConvertStr('10 pounds', 10,     'USD', '100 $', "note the middle space. and that the order number, space, symbol is kept");
    doConvertStr('10 pounds',  1.5,   'USD',  '15 $');
    doConvertStr('10 pounds',  1,     'USD',  '10 $');
    
    dummyRate=0.99999999;
    doConvertStr('10    pounds',  dummyRate, 'USD',  '10    $',  'rounded to 2 sig figs 0.999 = 10');
    doConvertStr('10.0  pounds',  dummyRate, 'USD',  '10.00  $');
    doConvertStr('10.00 pounds',  dummyRate, 'USD',  '10.00 $',  'stay with 2 decimals');
    
    doConvertStr('10 pounds',  dummyRate,   'GBP',  '10 pounds'); // no trailing space - identical
    
    doConvertStr('1 pound',    dummyRate,   'USD',  '1.00 $', '"pound" as well as "pounds" detected');
  };
  //doUKpounds(); //WILL: put this back in once I have seperate settings for .uk and .com

  
  // WILL: Note that source and target lists are changed here - it is no longer like typical extension setup

  // check that longer currency symbols are detected first - despite their order in the currencyList
  dummyRate=2;
  setSourceTo( ['ZAR','INR'] );

  doConvertStr('1 quid', dummyRate,   'USD',  NO_CHANGE, ' GBP are not in the current sourceCurrencyList - so no change');

  doConvertStr('1 R',    dummyRate,   'USD',  '2.00 $', ' one South African Rand - 1st in list');
  doConvertStr('1 Rs',   dummyRate,   'USD',  '2.00 $', ' one Indian Rupee - AFTER Rand in list');

  setSourceTo(['INR','ZAR']);

  doConvertStr('1 R',    dummyRate,   'USD',  '2.00 $', ' one South African Rand - 2nd in list');
  doConvertStr('1 Rs',   dummyRate,   'USD',  '2.00 $', ' one Indian Rupee - before Rand in list');

  setSourceTo( ['USD','GBP','EUR'] );


  // use the dollar and the rand because they are both one printable char
  doConvertStr('1$1',     20, 'ZAR', '1$1',        'ambigious values are not converted');
  doConvertStr('1 $ 1',   20, 'ZAR', '1 $ 1',      'ambigious values are not converted - with space');
  doConvertStr(' 1 $ 1',  20, 'ZAR', ' 1 $ 1',     'ambigious values are not converted - space before');
  doConvertStr('1  $  1', 20, 'ZAR', '1  $  1',    'ambigious values are not converted - two spaces');

  doConvertStr('1    $    1',      20, 'ZAR', '1    $    1',         'ambigious values are not converted - 4 spaces');
  doConvertStr('1    $   1',       20, 'ZAR', '1    R   20',          'OK - converts and preserves space');
  //foo='2'; //break into debugger
  doConvertStr('\n1     $    1',     20, 'ZAR', '\n1     R    20',        'Regression test - A this used to convert twice!');
  doConvertStr('   1      $    1', 20, 'ZAR', '   1      R    20',    'Regression test - B this used to convert twice!');


  setSourceTo( ['ZAR'] );

  doConvertStr('1R', 1, 'USD', '1$',    'regression test - check rand to dollar');
  doConvertStr('1R1', 1, 'USD', NO_CHANGE,  'regression test - 1R1 is ambigious so no conversion');
  doConvertStr('\n-->   1      R    1<--', 1, 'USD', '\n-->   1      $    1<--',    'regression test - this used to be too complicated for my simple RegExp');

  // test bugs:
  // * $6.9bn not converted
  // * "cost of $55, or ... not converted
  setSourceTo( ['USD'] );
  doConvertStr('$55,',   2,  'ZAR', 'R110,',    'BUG: Comma  should not stop conversion');
  doConvertStr('$6.9 bn', 2,  'ZAR', 'R13.80 bn', 'This will work OK since there is a space',true,true); //hideOriginal, turnOffRounding
  doConvertStr('$6.9bn', 2,  'ZAR', 'R13.80bn', 'BUG: Billion should not stop conversion',true,true);

  doConvertStr('$60-$70', 2,  'ZAR', 'R120-R140', 'BUG: Jesse found problems with hypen');

  doConvertStr('US$1 billion', 10,'ZAR', 'R10 billion', 'BUG, USD was not matched');

  // test conversion of 'AUD $'
  // http://www.dvdcrave.com/products/product.jsp?pid=19523
  setSourceTo( ['AUD','USD'] ); // USD has $ symbol
  //NOTE that each of these is rounded
  doConvertStr('AUD 123',  2, 'ZAR', 'R 246', 'Using code for AUD should be OK.'); //WILL: TODO: should there really be decimal points here?
  doConvertStr('AUD $123', 2, 'ZAR', 'R246', 'Test symbol AUD $, (no space since $ beside numbers)');
  doConvertStr('AUD$123',  2, 'ZAR', 'R246', 'Test symbol AUD$');
  
}


//****************** Test VMC.CurrencyList ************************************

function testCurrencyList() {
  if (!traceTest('Test the VMC.CurrencyList class')) { return; }
  info('WILL: TODO: I should test all public CurrencyList methods');

  var comment;
  var list = VMC.currencyDB.getCompleteList();
  
  var USD=list.findFromCode('USD');
  assertValidRef('found a USD',USD);
  //info('Found '+USD.toString(false)); //verbose toString

  var USD_virgin_islands = VMC.currencyDB.createListFromCountry('British Virgin Islands').items[0];
  assertEquals('first USD is USD_virgin_islands',USD_virgin_islands,USD);

  var USD_united_states = VMC.currencyDB.createListFromCountry('United States').items[0];
  assertNotEquals('first USD is not American',USD,USD_united_states);
 
  var countries=['Eurozone','United Kingdom'];
  var currencyList = VMC.currencyDB.createListFromCountries(countries);
  assertValidRef('got a currency list',currencyList);
  assertEquals('currency list length same as country list',countries.length,currencyList.items.length);
  //info('Found '+currencyList);


}

function TODOtestCurrencyListRegexpOrder() { // WILL: TODO: this test has been missing for some time
  if (!traceTest('Check that currency list matches biggest items first')) { return; }
  var comment;

  var list = VMC.currencyDB.getCompleteList();

  var re=list.getCurrencyRegExp();
  assertValidRef('is regexp defined',re);

  var r=re.exec('US $');
}







//****************** Test VMC.Currency ****************************************

function strArgs() {
  var sep='-';
  var args = [];
  this.VMC.applyFuncToArray( function(item) { args.push(item); },arguments); // JS annoyance: arguments is not a real array

  var str=args.toString().replace(/,/g,sep);
  //alert(str);
  return str;
}
function testCreateValidCurrency() {
  if (!traceTest('Try valid combinations for creating a VMC.Currency')) { return; }
  var comment;
  var formula = null;
  var p1 = assertCreateObject('OK pound',  VMC.Currency,'UK','Pound','GBP','quid',  formula);
  var p2 = assertCreateObject('OK pound',  VMC.Currency,'GB','Pound','GBP','pound', formula);
  var p3 = assertCreateObject('OK pound',  VMC.Currency,'English','Pound Sterling','GBP','pound', formula);
  var c1 = assertCreateObject('Different', VMC.Currency,'English','Pound Sterling','UKP','pound', formula);
  comment='Only Codes are compared for equality';
  assertTrue(comment,  p1.equals(p1));
  assertTrue(comment,  p1.equals(p2));
  assertTrue(comment,  p1.equals(p3));
  assertFalse(comment, p1.equals(c1));

  assertTrue(comment,  p2.equals(p1));
  assertTrue(comment,  p2.equals(p2));
  assertTrue(comment,  p2.equals(p3));
  assertFalse(comment, p2.equals(c1));

  assertTrue(comment,  p3.equals(p1));
  assertTrue(comment,  p3.equals(p2));
  assertTrue(comment,  p3.equals(p3));
  assertFalse(comment, p3.equals(c1));

  assertFalse(comment, c1.equals(p1));
  assertFalse(comment, c1.equals(p2));
  assertFalse(comment, c1.equals(p3));
  assertTrue(comment,  c1.equals(c1));

  
  //WILL: TODO: test all the other methods!!
}

function testAttemptInvalidCurrency() {
  if (!traceTest('Try invalid args for VMC.Number constructor')) { return; }
  var comment;
  //VMC.Currency(country,description,code,displaySymbolIndex,symbols) {
  assertFailCreateObject('null',            VMC.Currency,null);
  assertFailCreateObject('undefined',       VMC.Currency,undefined);
  assertFailCreateObject('no args',         VMC.Currency);
  assertFailCreateObject('empty',           VMC.Currency,'');
  
  comment='too few args';
  assertFailCreateObject(comment,       VMC.Currency,'UK');
  assertFailCreateObject(comment,       VMC.Currency,'UK','Pound');
  assertFailCreateObject(comment,       VMC.Currency,'UK','Pound','GBP');
  assertFailCreateObject(comment,       VMC.Currency,'UK','Pound','GBP',0);

  comment='too few args (from string)';
  assertFailCreateObject(comment,       VMC.Currency,'UK','Pound');
  assertFailCreateObject(comment,       VMC.Currency,strArgs('UK','Pound','GBP'));
}















//****************** Test VMC.Number ******************************************


function checkNumber(performRounding,str,expectedStr,comment) {
  var oldPerformRounding = VMC.Number.prototype.performRounding;
  VMC.Number.prototype.performRounding = performRounding;

  if (!comment) {comment=' Create '+str; }
  var num = assertCreateObject( comment, VMC.Number,str);
  assertEquals(comment+' '+str+'=='+expectedStr,  expectedStr,  num.toString());

  VMC.Number.prototype.performRounding = oldPerformRounding;
}

function checkMultiply(str,mult,expectedStr,comment,turnOffRounding) {
  var oldPerformRounding = VMC.Number.prototype.performRounding;
  if (turnOffRounding) { VMC.Number.prototype.performRounding = false; }

  if (!comment) {comment='Multiplying '+str+' by '+mult; }  
  var num = assertCreateObject( comment, VMC.Number,str);
  num.multiply(mult);
  assertEquals(comment + ': multiply '+str+' x '+mult,      expectedStr,  num.toString());

  VMC.Number.prototype.performRounding = oldPerformRounding;
}



function testRoundNumber() {
  if (!traceTest('Check that numbers are rounded correctly')) { return; }
  checkNumber( true,     '0.1',        '0.1'       ,'<10');
  checkNumber( true,     '0.99',       '0.99'      ,'<10');
  checkNumber( true,     '9.99',       '9.99'      ,'<10');
  checkNumber( true,     '10.01',      '10'        ,'>10 rounded');
  checkNumber( true,     '10.99',      '11'        ,'>10 rounded up');
  checkNumber( true,     '1045.51',    '1,046'     ,'>10 rounded up');
}

var round = true;
function testUnusualNumbers() {
  if (!traceTest('Unusual numbers')) { return; }
  checkNumber( round,    '0.1',        '0.1'       );
  checkNumber( round,    '0.01',       '0.01'      );
  checkNumber( round,    '0.001',      '0.001'     );
  checkNumber( round,    '0.0001',     '0.0001'    );
  checkNumber( round,    '0.00001',    '0.00001'   );
  checkNumber( round,    '0.000001',   '0.000001'  );
  checkNumber( round,    '0.0000001',  '???', 'very small numbers are not supported. WILL: TODO: do they appear in normal websites?');
  checkNumber( round,    '0.00000001', '???', 'very small numbers are not supported.');

  checkNumber( round,    '0',       '0', 'check that 0=0' );
  checkNumber( round,    '00000',   '0', 'check that lots of zeros is still 0' );


  var comment = 'negative not supported. WILL: TODO: do they appear in normal websites?';
  assertFailCreateObject(comment, VMC.Number,'-1');
}

function testSpecialRuleForThousandsGroup() {

  if (!traceTest('Check thousands auto-detect')) { return; }
  var comment;
  comment='if there are THREE digits right of a SINGLE comma or point then we assume that they are a THOUSANDS group';
  checkNumber( round,    '1,234',      '1,234', comment );
  checkNumber( round,   '12,234',     '12,234', comment );
  checkNumber( round,  '123,234',    '123,234', comment );
  checkNumber( round,    '1,23',       '1.23',  'not 3 digits' );
  checkNumber( round,'1,234.567',  '1,235',     'the comma is the thousands separator' );
  checkNumber( false,'1,234.567',  '1,234.567', 'the comma is the thousands separator' );

  checkNumber( round,    '1.234',      '1,234', comment );
  checkNumber( round,   '12.234',     '12,234', comment );
  checkNumber( round,  '123.234',    '123,234', comment );
  checkNumber( round,    '1.23',       '1.23',  'not 3 digits');
  checkNumber( round,'1.234,567',  '1,235'    , 'the point is the thousands separator');
  checkNumber( false,'1.234,567',  '1,234.567', 'the point is the thousands separator');

  // swiss spaces are always thousands separators
  checkNumber( round,'1 234,567',  '1,235',     comment ); 
  checkNumber( false,'1 234.567',  '1,234.567', comment ); 
}

function testAttemptInvalidNumber() {
  
  if (!traceTest('Try to create invalid VMC.Number')) { return; }
  assertFailCreateObject('null',            VMC.Number,null);
  assertFailCreateObject('undefined',       VMC.Number,undefined);
  assertFailCreateObject('no args',         VMC.Number);
  assertFailCreateObject('empty',           VMC.Number,'');
  assertFailCreateObject('only text',       VMC.Number,'x');
  assertFailCreateObject('only text',       VMC.Number,'this is lots of text but there are no numbers in it');
  assertFailCreateObject('non-digit start', VMC.Number,'$2 -  text must start with a number');
}

function testNumberTextUK() {

  if (!traceTest('Test UK format of numbers: WILL: TODO:')) { return; }
  checkNumber( round,      '2quid',         '2', 'test UK format'); 
      

  //WILL: TODO: stuff like this
  //      checkNumber( round,      '123,456.789.123',         '2');  //WILL: It's probably better to fail than to return
}

function testMultiply() {

  if (!traceTest('Test multiplying numbers: WILL: TODO:')) { return; }
  //checkMultiply(str,       mult,expectedStr, comment)


  checkMultiply('2.1',       2,   '4.20',  'after mult we need one extra sig fig'); 
    

  checkMultiply('2',         2,     '4.00'  ,'WILL: TODO: think about this, the answer is exactly 4 - so we do not need more sig figs');
  checkMultiply('2',         1.3,   '2.60'  ,'never just use one decimal place - $2.30 looks better than $2.3');
  checkMultiply('2000',      1.3,   '2,600');    
  checkMultiply('2000',      1.33,  '2,700','note how 2660 is rounded to 2700 since only 2 sig figs are wanted (there was only one in the original)');
    
  var rate=0.999999999;
  checkMultiply(      '10',     rate,       '10',       '(1 sigfig) ans=9.99999999 --> 10 (2 sigfigs)'        );
  checkMultiply(      '10.0',   rate,       '10.00',    '(2 sigfig) ans=9.99999999 --> 10.00 (3 sigfigs)'     );
  checkMultiply(      '10.00',  rate,       '10.00',    '(original had 2 decimal sigfigs - so stay with that' );
  checkMultiply(      '10.000', rate,   '10,000',       '10.000 treated as 10k - ans accurate to 2 sig figs'  );
  checkMultiply( '10.0000',     rate,       '10.00000', '(5 sigfig) ans=9.99999999 --> 10.00000 (2 sigfigs)'  );


  checkMultiply('52',       1,   '52',     'after mult we normally need one extra sig fig - but new==orig so no change'); 
  checkMultiply('23.45',    1,   '23',     '>10 so rounded');
  checkMultiply('23.45',    1,   '23.45',  'after mult we normally need one extra sig fig - but new==orig so no change', true /* turn off rounding */); 


  //WILL: TODO: so many more checks needed!
    
}

function testNumberUK() {
  if (!traceTest('Test UK format')) { return; }
  //checkNumber( round,str,  expectedStr, comment) {

  var comment;
  comment='UK plain';
  checkNumber( round,      '2',         '2',   comment );
  checkNumber( round,     '23',        '23',   comment );
  checkNumber( round,    '230',       '230',   comment );
  checkNumber( round,   '2300',      '2,300',  comment );
  checkNumber( round,  '23000',     '23,000',  comment );
  checkNumber( round, '230000',    '230,000',  comment );
  checkNumber( round,'2300000',   '2,300,000', comment );

  comment='UK with thousands separator';
  checkNumber( round,        '2',           '2',  comment );
  checkNumber( round,       '23',          '23',  comment );
  checkNumber( round,      '230',         '230',  comment );
  checkNumber( round,    '2,300',       '2,300',  comment );
  checkNumber( round,   '23,000',      '23,000',  comment );
  checkNumber( round,  '230,000',     '230,000',  comment );
  checkNumber( round,'2,300,000',   '2,300,000',  comment );

  comment='UK with decimal separator - output should have same number of decimal places since number has not changed';
  checkNumber( round,      '2.0',            '2.0'     ,  comment );
  checkNumber( round,     '23.10',          '23'       ,  comment );
  checkNumber( false,     '23.10',          '23.10'    ,  comment );
  checkNumber( round,    '230.234',    '230,234'       ,  'special thousands grouping rule. Currencies in UK format with 3 decimal digits will fail :-(');
  checkNumber( round,   '2300.23456',    '2,300'       ,  comment );
  checkNumber( false,   '2300.23456',    '2,300.23456' ,  comment );
  checkNumber( round,  '23000.9',       '23,001'       ,  comment );
  checkNumber( false,  '23000.9',       '23,000.9'     ,  comment );
  checkNumber( round, '230000.99',     '230,001'       ,  comment );
  checkNumber( false, '230000.99',     '230,000.99'    ,  comment );
  checkNumber( round,'2300000.0',    '2,300,000'       ,  comment );
  checkNumber( false,'2300000.0',    '2,300,000.0'     ,  comment );

  comment='UK with thousands and decimal separators';
  checkNumber( round,        '2.0',            '2.0'     ,  comment );
  checkNumber( round,       '23.10',          '23'	 ,  comment );
  checkNumber( false,       '23.10',          '23.10'    ,  comment );
  checkNumber( round,      '230.234',    '230,234'       ,  'special thousands grouping rule. Currencies in UK format with 3 decimal digits will fail :-(');
  checkNumber( round,    '2,300.23456',    '2,300'	 ,  comment );
  checkNumber( false,    '2,300.23456',    '2,300.23456' ,  comment );
  checkNumber( round,   '23,000.9',       '23,001'	 ,  comment );
  checkNumber( false,   '23,000.9',       '23,000.9'     ,  comment );
  checkNumber( round,  '230,000.99',     '230,001'	 ,  comment );
  checkNumber( false,  '230,000.99',     '230,000.99'    ,  comment );
  checkNumber( round,'2,300,000.0',    '2,300,000'	 ,  comment );
  checkNumber( false,'2,300,000.0',    '2,300,000.0'     ,  comment );

  checkNumber( round,'111,111.11',     '111,111'	 , 'Regression test: I thought xxx,xxx.xx format not regconised - but it is');
  checkNumber( false,'111,111.11',     '111,111.11'	 , 'Regression test: I thought xxx,xxx.xx format not regconised - but it is');
}

function testNumberUKoutputBelgian() {
  if (!traceTest('Test UK formatted numbers as input, output as Belgian (thousands=. decimal=,)')) { return; }
  //checkNumber( round,str,  expectedStr, comment) {

  var oldThousands = VMC.Number.prototype.thousandsSeparator;
  var oldDecimal   = VMC.Number.prototype.decimalSeparator;

  // set belgian style
  VMC.Number.prototype.thousandsSeparator='.';
  VMC.Number.prototype.decimalSeparator=',';

  try {
    var comment;
    comment='UK plain';
    checkNumber( round,      '2',         '2',   comment );
    checkNumber( round,     '23',        '23',   comment );
    checkNumber( round,    '230',       '230',   comment );
    checkNumber( round,   '2300',      '2.300',  comment );
    checkNumber( round,  '23000',     '23.000',  comment );
    checkNumber( round, '230000',    '230.000',  comment );
    checkNumber( round,'2300000',   '2.300.000', comment );
    
    comment='UK with thousands separator';
    checkNumber( round,        '2',           '2',  comment );
    checkNumber( round,       '23',          '23',  comment );
    checkNumber( round,      '230',         '230',  comment );
    checkNumber( round,    '2,300',       '2.300',  comment );
    checkNumber( round,   '23,000',      '23.000',  comment );
    checkNumber( round,  '230,000',     '230.000',  comment );
    checkNumber( round,'2,300,000',   '2.300.000',  comment );
    
    comment='UK with decimal separator - output should have same number of decimal places since number has not changed';
    checkNumber( round,      '2.0',            '2,0'     ,  comment );
    checkNumber( round,     '23.10',          '23'	 ,  comment );
    checkNumber( false,     '23.10',          '23,10'    ,  comment );
    checkNumber( round,    '230.234',    '230.234'       ,  'special thousands grouping rule. Currencies in UK format with 3 decimal digits will fail :-(');
    checkNumber( round,   '2300.23456',    '2.300'	 ,  comment );
    checkNumber( false,   '2300.23456',    '2.300,23456' ,  comment );
    checkNumber( round,  '23000.9',       '23.001'	 ,  comment );
    checkNumber( false,  '23000.9',       '23.000,9'     ,  comment );
    checkNumber( round, '230000.99',     '230.001'	 ,  comment );
    checkNumber( false, '230000.99',     '230.000,99'    ,  comment );
    checkNumber( round,'2300000.0',    '2.300.000'	 ,  comment );
    checkNumber( false,'2300000.0',    '2.300.000,0'     ,  comment );
    
    comment='UK with thousands and decimal separators';
    checkNumber( round,        '2.0',            '2,0'     ,  comment );
    checkNumber( round,       '23.10',          '23'	   ,  comment );
    checkNumber( false,       '23.10',          '23,10'    ,  comment );
    checkNumber( round,      '230.234',    '230.234'       ,  'special thousands grouping rule. Currencies in UK format with 3 decimal digits will fail :-(');
    checkNumber( round,    '2,300.23456',    '2.300'	   ,  comment );
    checkNumber( false,    '2,300.23456',    '2.300,23456' ,  comment );
    checkNumber( round,   '23,000.9',       '23.001'	   ,  comment );
    checkNumber( false,   '23,000.9',       '23.000,9'     ,  comment );
    checkNumber( round,  '230,000.99',     '230.001'	   ,  comment );
    checkNumber( false,  '230,000.99',     '230.000,99'    ,  comment );
    checkNumber( round,'2,300,000.0',    '2.300.000'	   ,  comment );
    checkNumber( false,'2,300,000.0',    '2.300.000,0'     ,  comment );
    
    checkNumber( round,'111,111.11',     '111.111'	   , 'Regression test: I thought xxx,xxx.xx format not regconised - but it is');
    checkNumber( false,'111,111.11',     '111.111,11'	   , 'Regression test: I thought xxx,xxx.xx format not regconised - but it is');
  } finally {
    //restore previous number format
    VMC.Number.prototype.thousandsSeparator = oldThousands;
    VMC.Number.prototype.decimalSeparator   = oldDecimal;
  }
}

function testNumberEuropean() {
  if (!traceTest('Test European format')) { return; }

  //checkNumber( round,str,  expectedStr) {

  // EU plain is just the same as UK - skipping

  var comment;
  comment='EU with thousands separator';
  checkNumber( round,        '2',           '2',  comment );
  checkNumber( round,       '23',          '23',  comment );
  checkNumber( round,      '230',         '230',  comment );
  checkNumber( round,    '2.300',       '2,300',  comment );
  checkNumber( round,   '23.000',      '23,000',  comment );
  checkNumber( round,  '230.000',     '230,000',  comment );
  checkNumber( round,'2.300.000',   '2,300,000',  comment );

  comment='EU with decimal separator - output should have same number of decimal places since number has not changed';
  checkNumber( round,      '2,0',            '2.0'     ,  comment );
  checkNumber( round,     '23,10',          '23'       ,  comment );
  checkNumber( false,     '23,10',          '23.10'    ,  comment );
  checkNumber( round,    '230,234',    '230,234'       ,  'special thousands grouping rule. Currencies in EU format with 3 decimal digits will fail :-(');
  checkNumber( round,   '2300,23456',    '2,300'       ,  comment );
  checkNumber( false,   '2300,23456',    '2,300.23456' ,  comment );
  checkNumber( round,  '23000,9',       '23,001'       ,  comment );
  checkNumber( false,  '23000,9',       '23,000.9'     ,  comment );
  checkNumber( round, '230000,99',     '230,001'       ,  comment );
  checkNumber( false, '230000,99',     '230,000.99'    ,  comment );
  checkNumber( round,'2300000,0',    '2,300,000'       ,  comment );
  checkNumber( false,'2300000,0',    '2,300,000.0'     ,  comment );

  comment='EU with thousands and decimal separators';
  checkNumber( round,        '2,0',            '2.0'	 ,  comment );
  checkNumber( round,       '23,10',          '23'	 ,  comment );
  checkNumber( false,       '23,10',          '23.10'	 ,  comment );
  checkNumber( round,      '230,234',    '230,234'	 , 'special thousands grouping rule. Currencies in EU format with 3 decimal digits will fail :-(');
  checkNumber( round,    '2.300,23456',    '2,300'	 ,  comment );
  checkNumber( false,    '2.300,23456',    '2,300.23456' ,  comment );
  checkNumber( round,   '23.000,9',       '23,001'	 ,  comment );
  checkNumber( false,   '23.000,9',       '23,000.9'     ,  comment );
  checkNumber( round,  '230.000,99',     '230,001'	 ,  comment );
  checkNumber( false,  '230.000,99',     '230,000.99'    ,  comment );
  checkNumber( round,'2.300.000,0',    '2,300,000'	 ,  comment );
  checkNumber( false,'2.300.000,0',    '2,300,000.0'     ,  comment );
}

function testNumberSwiss() {
  if (!traceTest('Test Swiss format')) { return; }

  var comment;
  //checkNumber( round,str,          expectedStr, comment)

  // Swiss plain is just the same as UK - skipping

  comment = ' Swiss with thousands separator';
  checkNumber( round,        '2',           '2',  comment );
  checkNumber( round,       '23',          '23',  comment );
  checkNumber( round,      '230',         '230',  comment );
  checkNumber( round,    '2 300',       '2,300',  comment );
  checkNumber( round,   '23 000',      '23,000',  comment );
  checkNumber( round,  '230 000',     '230,000',  comment );
  checkNumber( round,'2 300 000',   '2,300,000',  comment );

  //Swiss with decimal separator is the same as EU and UK together - skipping

  comment = 'Swiss with thousands and COMMA decimal separators';
  checkNumber( round,        '2,0',            '2.0'     ,  comment );
  checkNumber( round,       '23,10',          '23'       ,  comment );
  checkNumber( false,       '23,10',          '23.10'    ,  comment );
  checkNumber( round,      '230,234',    '230,234'       ,  'special thousands grouping rule. Currencies in Swiss format with 3 decimal digits will fail :-(');
  checkNumber( round,    '2 300,23456',    '2,300'       ,  comment );
  checkNumber( false,    '2 300,23456',    '2,300.23456' ,  comment );
  checkNumber( round,   '23 000,9',       '23,001'	 ,  comment );
  checkNumber( false,   '23 000,9',       '23,000.9'     ,  comment );
  checkNumber( round,  '230 000,99',     '230,001'	 ,  comment );
  checkNumber( false,  '230 000,99',     '230,000.99'    ,  comment );
  checkNumber( round,'2 300 000,0',    '2,300,000'	 ,  comment );
  checkNumber( false,'2 300 000,0',    '2,300,000.0'     ,  comment );

  comment = 'Swiss with thousands and POINT decimal separators';
  checkNumber( round,        '2.0',            '2.0'     ,  comment );
  checkNumber( round,       '23.10',          '23'	 ,  comment );
  checkNumber( false,       '23.10',          '23.10'    ,  comment );
  checkNumber( round,      '230.234',    '230,234'       ,  'special thousands grouping rule. Currencies in Swiss format with 3 decimal digits will fail :-(');
  checkNumber( round,    '2 300.23456',    '2,300'	 ,  comment );
  checkNumber( false,    '2 300.23456',    '2,300.23456' ,  comment );
  checkNumber( round,   '23 000.9',       '23,001'	 ,  comment );
  checkNumber( false,   '23 000.9',       '23,000.9'     ,  comment );
  checkNumber( round,  '230 000.99',     '230,001'	 ,  comment );
  checkNumber( false,  '230 000.99',     '230,000.99'    ,  comment );
  checkNumber( round,'2 300 000.0',    '2,300,000'	 ,  comment );
  checkNumber( false,'2 300 000.0',    '2,300,000.0'     ,  comment );
}


function testNumberSwissQuote() {
  if (!traceTest('Test Swiss format, using Quote as Thousands separator')) { return; }

  var comment;
  //checkNumber( round,str,          expectedStr, comment)

  // Swiss plain is just the same as UK - skipping

  comment = " Swiss with quote thousands separator";
  checkNumber( round,        "2",           "2",  comment );
  checkNumber( round,       "23",          "23",  comment );
  checkNumber( round,      "230",         "230",  comment );
  checkNumber( round,    "2'300",       "2,300",  comment );
  checkNumber( round,   "23'000",      "23,000",  comment );
  checkNumber( round,  "230'000",     "230,000",  comment );
  checkNumber( round,"2'300'000",   "2,300,000",  comment );

  //Swiss with decimal separator is the same as EU and UK together - skipping

  comment = "Swiss with thousands and POINT decimal separators";
  checkNumber( round,        "2.0",            "2.0"     ,  comment );
  checkNumber( round,       "23.10",          "23"       ,  comment );
  checkNumber( false,       "23.10",          "23.10"    ,  comment );
  checkNumber( round,      "230.234",    "230,234"       ,  "special thousands grouping rule. Currencies in Swiss format with 3 decimal digits will fail :-(");
  checkNumber( round,    "2'300.23456",    "2,300"       ,  comment );
  checkNumber( false,    "2'300.23456",    "2,300.23456" ,  comment );
  checkNumber( round,   "23'000.9",       "23,001"       ,  comment );
  checkNumber( false,   "23'000.9",       "23,000.9"     ,  comment );
  checkNumber( round,  "230'000.99",     "230,001"       ,  comment );
  checkNumber( false,  "230'000.99",     "230,000.99"    ,  comment );
  checkNumber( round,"2'300'000.0",    "2,300,000"       ,  comment );
  checkNumber( false,"2'300'000.0",    "2,300,000.0"     ,  comment );
}

//****************** END OF testS **********************************************
//****************** END OF testS **********************************************

